package cn.shabby;

import org.apache.pdfbox.contentstream.PDContentStream;
import org.apache.pdfbox.contentstream.PDFStreamEngine;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.contentstream.operator.OperatorProcessor;
import org.apache.pdfbox.contentstream.operator.text.*;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
import org.apache.pdfbox.util.*;
import org.apache.pdfbox.util.Vector;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Created by Administrator on 2018/3/28.
 */
public class TextWaterMarkFilter extends PDFStreamEngine {

    private PDPage page;

    private StringBuilder stringBuilder;

    private final Map<String, OperatorProcessor> operators = new HashMap<String, OperatorProcessor>(80);

    private boolean isShowText = false;

    Method initPage;

    Method pushResources;

    Method popResources;

    ArrayList<StringFilter> stringFilters = new ArrayList<>();

    ParserProxy proxy;

    public TextWaterMarkFilter(PDPage page, ArrayList<StringFilter> stringFilters, boolean isShowText) throws NoSuchMethodException {
        this(page, stringFilters);
        this.isShowText = isShowText;
    }

    public TextWaterMarkFilter(PDPage page, ArrayList<StringFilter> stringFilters) throws NoSuchMethodException {
        this(page);
        if(stringFilters!=null)
            this.stringFilters = stringFilters;
    }

    /**
     * Constructor.
     *
     * @param page
     */
    protected TextWaterMarkFilter(PDPage page) throws NoSuchMethodException {
        setPage(page);
        addSelfOperator(new BeginText());
        addSelfOperator(new SetFontAndSize());
        addSelfOperator(new EndText());
        addSelfOperator(new ShowText());
        addSelfOperator(new ShowTextAdjusted());
        addSelfOperator(new ShowTextLine());

        initPage = getClass().getSuperclass().getDeclaredMethod("initPage", PDPage.class);
        initPage.setAccessible(true);

        pushResources = getClass().getSuperclass().getDeclaredMethod("pushResources", PDContentStream.class);
        pushResources.setAccessible(true);

        popResources = getClass().getSuperclass().getDeclaredMethod("popResources", PDResources.class);
        popResources.setAccessible(true);

    }

    public PDPage getPage() {
        return page;
    }

    public void setPage(PDPage page) {
        this.page = page;
    }

    public void addFilter(StringFilter filter){
        stringFilters.add(filter);
    }

    /**
     * Runs the engine on the current page.
     *
     * @throws IOException If there is an IO error while drawing the page.
     */
    public void run() throws IOException, InvocationTargetException, IllegalAccessException {
        Long start=new Date().getTime();
        initPage(page);
        processStream(page);
    }

    public final void addSelfOperator(OperatorProcessor op)
    {
        op.setContext(this);
        operators.put(op.getName(), op);
    }

    @Override
    public void beginText() throws IOException {
        stringBuilder = new StringBuilder();
        proxy.setBegin(proxy.getPosition()-2);
    }

    @Override
    public void endText() throws IOException {
        String string = stringBuilder.toString();
        proxy.setEnd(proxy.getPosition()+1);
        try {
            for(StringFilter filter:stringFilters){
                filter.doFilter(string, getInitialMatrix());
            }
        }catch (FilterReplaceException e){
            System.out.println("FilterReplaceException: "+e.getMessage());
            proxy.replace();
        }
        if(isShowText)
             System.out.println(string);
    }

    protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode,
                             Vector displacement) throws IOException
    {
        stringBuilder.append(unicode);
    }



    /**
     * Processes the operators of the given content stream.
     *
     * @param contentStream to content stream to parse.
     * @throws IOException if there is an error reading or parsing the content stream.
     */
    public void processStreamOperators(PDContentStream contentStream) throws IOException
    {
        proxy = new ParserProxy(contentStream);
        processAsParser(proxy.getParser());
    }

    /**
     * Processes the operators of the given content stream.
     *
     * @param cosStream to content stream to parse.
     * @throws IOException if there is an error reading or parsing the content stream.
     */
    public void processStreamOperators(PDStream cosStream) throws IOException
    {
        proxy = new ParserProxy(cosStream);
        processAsParser(proxy.getParser());
    }

    public void processAsParser(PDFStreamParser parser) throws IOException {
        List<COSBase> arguments = new ArrayList<COSBase>();
        Object token = parser.parseNextToken();

        while (token != null)
        {
            if (token instanceof COSObject)
            {
                arguments.add(((COSObject) token).getObject());
            }
            else if (token instanceof Operator)
            {
                processOperator((Operator) token, arguments);
                arguments = new ArrayList<COSBase>();
            }
            else
            {
                arguments.add((COSBase) token);
            }
            token = parser.parseNextToken();
        }
    }

    protected void processOperator(Operator operator, List<COSBase> operands) throws IOException
    {
        String name = operator.getName();
        OperatorProcessor processor = operators.get(name);
        if (processor != null)
        {
            processor.setContext(this);
            try
            {
                processor.process(operator, operands);
            }
            catch (IOException e)
            {
                operatorException(operator, operands, e);
            }
        }
        else
        {
            unsupportedOperator(operator, operands);
        }
    }

    /**
     * Process a content stream.
     *
     * @param contentStream the content stream
     * @throws IOException if there is an exception while processing the stream
     */
    private void processStream(PDContentStream contentStream) throws IOException, InvocationTargetException, IllegalAccessException {
        PDResources parent = pushResources(contentStream);
        Stack<PDGraphicsState> savedStack = saveGraphicsStack();
        processStreamOperators(contentStream);
        restoreGraphicsStack(savedStack);
        popResources(parent);
    }

    void initPage(PDPage o) throws InvocationTargetException, IllegalAccessException {
        initPage.invoke(this, o);
    }

    PDResources pushResources(PDContentStream o) throws InvocationTargetException, IllegalAccessException {
        return (PDResources) pushResources.invoke(this, o);
    }

    void  popResources(PDResources o) throws InvocationTargetException, IllegalAccessException {
        popResources.invoke(this, o);
    }


    //获取过滤后的资源
    class ParserProxy{

        PDFStreamParser parser;

        Object object;
        Method method;

        long begin=0L;

        long end=0L;

        int size = 1024;
        byte[] bytes = new byte[size];

        ByteArrayOutputStream byteArrayOutputStream;

        PDStream pdStream;



        public ParserProxy(PDContentStream pdContentStream) throws IOException {
            this(new PDFStreamParser(pdContentStream));
        }

        public ParserProxy(PDStream pdStream) throws IOException {
            this(new PDFStreamParser(pdStream));
            this.pdStream = pdStream;
            byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(pdStream.toByteArray());
        }


        public ParserProxy(PDFStreamParser parser) {
            this.parser = parser;
            try {
                Field field = parser.getClass().getSuperclass().getDeclaredField("seqSource");
                field.setAccessible(true);
                object = field.get(parser);
                method = object.getClass().getDeclaredMethod("getPosition");
                method.setAccessible(true);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }

        long getPosition(){
            try {
                return (long) method.invoke(object);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            return 0L;
        }

        public PDFStreamParser getParser() {
            return parser;
        }

        public long getBegin() {
            return begin;
        }

        public void setBegin(long begin) {
            this.begin = begin;
        }

        public long getEnd() {
            return end;
        }

        public void setEnd(long end) {
            this.end = end;
        }

        public void replace() throws IOException {
            byte[] before = byteArrayOutputStream.toByteArray();
            for (Long i = begin; i <= end; i++) {
                before[i.intValue()] = 32;
            }
            byteArrayOutputStream.reset();
            byteArrayOutputStream.write(before);
        }

        PDStream getPDStream() throws IOException {
           OutputStream outputStream = pdStream.createOutputStream();
           outputStream.write(byteArrayOutputStream.toByteArray());
           outputStream.flush();
           outputStream.close();
           return pdStream;
        }
    }

    PDStream getPDStream() throws IOException {
        return proxy.getPDStream();
    }

}
